UNPKG

debug-time-machine-core

Version:

๐Ÿ•ฐ๏ธ Debug Time Machine Core - ์‹œ๊ฐ„์—ฌํ–‰ ์—”์ง„๊ณผ ๋„คํŠธ์›Œํฌ ์ธํ„ฐ์…‰ํ„ฐ

1,686 lines (1,680 loc) โ€ข 157 kB
"use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var index_exports = {}; __export(index_exports, { APIStateMapper: () => APIStateMapper, AxiosInterceptor: () => AxiosInterceptor, DOMSnapshotOptimizer: () => DOMSnapshotOptimizer, DebugTimeMachineEngine: () => DebugTimeMachineEngine, ErrorDetector: () => ErrorDetector, MemoryManager: () => MemoryManager, MetricsCollector: () => MetricsCollector, NetworkInterceptor: () => NetworkInterceptor, RealTimeAPIInterceptor: () => RealTimeAPIInterceptor, RealTimeErrorReporter: () => RealTimeErrorReporter, TimeTravelEngine: () => TimeTravelEngine, UserActionDetector: () => UserActionDetector, WebSocketConnector: () => WebSocketConnector, createErrorBoundaryWithReporter: () => createErrorBoundaryWithReporter, createReactErrorBoundary: () => createReactErrorBoundary, debounce: () => debounce, deepClone: () => deepClone, destroyGlobalAPIInterceptor: () => destroyGlobalAPIInterceptor, destroyGlobalAPIStateMapper: () => destroyGlobalAPIStateMapper, destroyGlobalDebugTimeMachine: () => destroyGlobalDebugTimeMachine, destroyGlobalErrorReporter: () => destroyGlobalErrorReporter, formatTimestamp: () => formatTimestamp, generateUniqueId: () => generateUniqueId, getGlobalAPIInterceptor: () => getGlobalAPIInterceptor, getGlobalAPIStateMapper: () => getGlobalAPIStateMapper, getGlobalDebugTimeMachine: () => getGlobalDebugTimeMachine, getGlobalErrorDetector: () => getGlobalErrorDetector, getGlobalErrorReporter: () => getGlobalErrorReporter, getRelativeTime: () => getRelativeTime, isEmptyObject: () => isEmptyObject, startDebugTimeMachine: () => startDebugTimeMachine, stopDebugTimeMachine: () => stopDebugTimeMachine, throttle: () => throttle }); module.exports = __toCommonJS(index_exports); // src/utils/index.ts function generateUniqueId() { return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; } function deepClone(obj) { if (obj === null || typeof obj !== "object") { return obj; } if (obj instanceof Date) { return new Date(obj.getTime()); } if (obj instanceof Array) { return obj.map((item) => deepClone(item)); } if (typeof obj === "object") { const clonedObj = {}; for (const key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { clonedObj[key] = deepClone(obj[key]); } } return clonedObj; } return obj; } function debounce(func, wait) { let timeout = null; return (...args) => { if (timeout) { clearTimeout(timeout); } timeout = setTimeout(() => func(...args), wait); }; } function throttle(func, limit) { let inThrottle = false; return (...args) => { if (!inThrottle) { func(...args); inThrottle = true; setTimeout(() => { inThrottle = false; }, limit); } }; } function isEmptyObject(obj) { return Object.keys(obj).length === 0; } function formatTimestamp(timestamp) { return new Date(timestamp).toISOString(); } function getRelativeTime(timestamp) { const now = Date.now(); const diff = now - timestamp; const seconds = Math.floor(diff / 1e3); const minutes = Math.floor(seconds / 60); const hours = Math.floor(minutes / 60); const days = Math.floor(hours / 24); if (days > 0) return `${days}\uC77C \uC804`; if (hours > 0) return `${hours}\uC2DC\uAC04 \uC804`; if (minutes > 0) return `${minutes}\uBD84 \uC804`; return `${seconds}\uCD08 \uC804`; } // src/network-interceptor/NetworkInterceptor.ts var NetworkInterceptor = class { constructor(config = {}) { this._isIntercepting = false; this._requestHistory = []; this._responseHistory = []; this._config = { maxHistorySize: 1e3, captureRequestBody: true, captureResponseBody: true, enableTiming: true, enableGraphQLParsing: true, enableAxiosInterception: false, maxBodySize: 1024 * 1024, // 1MB excludePatterns: [ /chrome-extension:/, /moz-extension:/, /webkit-masked-url:/, /^data:/, /\.woff2?(\?|$)/, /\.ttf(\?|$)/, /favicon\.ico/ ], includePatterns: [], onRequest: void 0, onResponse: void 0, onError: void 0, ...config }; this._originalFetch = window.fetch.bind(window); this._originalXMLHttpRequest = window.XMLHttpRequest; } /** * ๋„คํŠธ์›Œํฌ ์š”์ฒญ ๊ฐ€๋กœ์ฑ„๊ธฐ ์‹œ์ž‘ */ startIntercepting() { if (this._isIntercepting) { console.warn("[NetworkInterceptor] Already intercepting"); return; } console.log("[NetworkInterceptor] Starting network interception..."); this._interceptFetch(); this._interceptXMLHttpRequest(); this._isIntercepting = true; console.log("[NetworkInterceptor] Network interception started"); } /** * ๋„คํŠธ์›Œํฌ ์š”์ฒญ ๊ฐ€๋กœ์ฑ„๊ธฐ ์ค‘๋‹จ */ stopIntercepting() { if (!this._isIntercepting) { return; } console.log("[NetworkInterceptor] Stopping network interception..."); window.fetch = this._originalFetch; window.XMLHttpRequest = this._originalXMLHttpRequest; this._isIntercepting = false; console.log("[NetworkInterceptor] Network interception stopped"); } /** * ์š”์ฒญ ํžˆ์Šคํ† ๋ฆฌ ๊ฐ€์ ธ์˜ค๊ธฐ */ getRequestHistory() { return [...this._requestHistory]; } /** * ์‘๋‹ต ํžˆ์Šคํ† ๋ฆฌ ๊ฐ€์ ธ์˜ค๊ธฐ */ getResponseHistory() { return [...this._responseHistory]; } /** * ํžˆ์Šคํ† ๋ฆฌ ์ดˆ๊ธฐํ™” */ clearHistory() { this._requestHistory = []; this._responseHistory = []; } /** * ๋„คํŠธ์›Œํฌ ์„ฑ๋Šฅ ๋งคํŠธ๋ฆญ ๊ฐ€์ ธ์˜ค๊ธฐ */ getNetworkMetrics() { const requests = this._requestHistory; const responses = this._responseHistory; if (requests.length === 0) { return { totalRequests: 0, successRate: 0, averageResponseTime: 0, totalDataTransferred: 0, errorCount: 0, slowRequestsCount: 0 }; } const completedRequests = requests.filter( (req) => responses.some((res) => res.requestId === req.id) ); const totalDataTransferred = responses.reduce( (sum, res) => sum + res.size.total, 0 ); const averageResponseTime = completedRequests.reduce( (sum, req) => sum + req.timing.duration, 0 ) / completedRequests.length; const errorCount = responses.filter((res) => res.status >= 400).length; const slowRequestsCount = completedRequests.filter( (req) => req.timing.duration > 3e3 ).length; return { totalRequests: requests.length, completedRequests: completedRequests.length, successRate: completedRequests.length > 0 ? (completedRequests.length - errorCount) / completedRequests.length * 100 : 0, averageResponseTime: Math.round(averageResponseTime), totalDataTransferred, errorCount, slowRequestsCount, graphqlRequestsCount: requests.filter((req) => req.graphql).length }; } /** * Fetch API ๊ฐ€๋กœ์ฑ„๊ธฐ */ _interceptFetch() { const self = this; window.fetch = async function(input, init) { const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; if (self._shouldExcludeRequest(url)) { return self._originalFetch.call(this, input, init); } const requestId = generateUniqueId(); const method = init?.method || "GET"; const startTime = performance.now(); const headers = self._extractHeaders(init?.headers); let requestBody; let requestBodySize = 0; if (init?.body && self._config.captureRequestBody) { if (typeof init.body === "string") { requestBody = init.body.length > self._config.maxBodySize ? init.body.substring(0, self._config.maxBodySize) + "...[truncated]" : init.body; requestBodySize = new TextEncoder().encode(init.body).length; } else { requestBody = "[Binary Data]"; requestBodySize = init.body?.length || 0; } } const graphqlInfo = self._parseGraphQL(headers, requestBody); const timing = { startTime, requestStart: startTime, responseStart: 0, responseEnd: 0, duration: 0 }; const networkRequest = { id: requestId, url, method, headers, body: requestBody, timestamp: Date.now(), timing, size: { requestHeaders: self._calculateHeadersSize(headers), requestBody: requestBodySize, total: self._calculateHeadersSize(headers) + requestBodySize }, graphql: graphqlInfo, priority: self._calculateRequestPriority(url, method, graphqlInfo) }; self._requestHistory.push(networkRequest); if (self._requestHistory.length > self._config.maxHistorySize) { self._requestHistory = self._requestHistory.slice(-self._config.maxHistorySize); } if (self._config.onRequest) { try { self._config.onRequest(networkRequest); } catch (error) { console.warn("[NetworkInterceptor] Error in onRequest callback:", error); } } try { timing.responseStart = performance.now(); const response = await self._originalFetch.call(this, input, init); timing.responseEnd = performance.now(); timing.duration = timing.responseEnd - timing.startTime; await self._handleFetchResponse(response, networkRequest); return response; } catch (error) { timing.responseEnd = performance.now(); timing.duration = timing.responseEnd - timing.startTime; await self._handleFetchError(error, networkRequest); throw error; } }; } /** * XMLHttpRequest ๊ฐ€๋กœ์ฑ„๊ธฐ */ _interceptXMLHttpRequest() { const self = this; const OriginalXHR = this._originalXMLHttpRequest; window.XMLHttpRequest = function XMLHttpRequestWrapper() { const xhr = new OriginalXHR(); let requestId = ""; let url = ""; let method = ""; let requestHeaders = {}; let startTime = 0; const originalOpen = xhr.open; const originalSend = xhr.send; const originalSetRequestHeader = xhr.setRequestHeader; xhr.open = function(httpMethod, httpUrl, ...args) { method = httpMethod; url = httpUrl.toString(); requestId = generateUniqueId(); return originalOpen.apply(this, [httpMethod, httpUrl, ...args]); }; xhr.setRequestHeader = function(name, value) { requestHeaders[name] = value; return originalSetRequestHeader.call(this, name, value); }; xhr.send = function(body) { if (self._shouldExcludeRequest(url)) { return originalSend.call(this, body); } startTime = performance.now(); let requestBody; let requestBodySize = 0; if (body && self._config.captureRequestBody) { if (typeof body === "string") { requestBody = body.length > self._config.maxBodySize ? body.substring(0, self._config.maxBodySize) + "...[truncated]" : body; requestBodySize = new TextEncoder().encode(body).length; } else { requestBody = "[Binary Data]"; requestBodySize = body?.length || 0; } } const graphqlInfo = self._parseGraphQL(requestHeaders, requestBody); const timing = { startTime, requestStart: startTime, responseStart: 0, responseEnd: 0, duration: 0 }; const networkRequest = { id: requestId, url, method, headers: requestHeaders, body: requestBody, timestamp: Date.now(), timing, size: { requestHeaders: self._calculateHeadersSize(requestHeaders), requestBody: requestBodySize, total: self._calculateHeadersSize(requestHeaders) + requestBodySize }, graphql: graphqlInfo, priority: self._calculateRequestPriority(url, method, graphqlInfo) }; self._requestHistory.push(networkRequest); if (self._config.onRequest) { try { self._config.onRequest(networkRequest); } catch (error) { console.warn("[NetworkInterceptor] Error in onRequest callback:", error); } } const originalOnReadyStateChange = xhr.onreadystatechange; xhr.onreadystatechange = function(event) { if (xhr.readyState === 4) { timing.responseEnd = performance.now(); timing.duration = timing.responseEnd - timing.startTime; self._handleXHRResponse(xhr, networkRequest); } if (originalOnReadyStateChange) { return originalOnReadyStateChange.call(this, event); } }; return originalSend.call(this, body); }; return xhr; }; Object.setPrototypeOf(window.XMLHttpRequest, OriginalXHR); Object.defineProperty(window.XMLHttpRequest, "prototype", { value: OriginalXHR.prototype, writable: false }); } /** * ์š”์ฒญ URL์ด ์ œ์™ธ ํŒจํ„ด์— ๋งค์น˜๋˜๋Š”์ง€ ํ™•์ธ */ _shouldExcludeRequest(url) { return this._config.excludePatterns.some((pattern) => pattern.test(url)); } /** * GraphQL ์ฟผ๋ฆฌ ํŒŒ์‹ฑ */ _parseGraphQL(headers, body) { if (!this._config.enableGraphQLParsing || !body) { return void 0; } try { const contentType = headers["content-type"] || headers["Content-Type"] || ""; if (contentType.includes("application/graphql")) { return { operationType: this._detectGraphQLOperationType(body), operationName: this._extractGraphQLOperationName(body) }; } if (contentType.includes("application/json")) { const parsed = JSON.parse(body); if (parsed.query || parsed.operationName) { return { operationType: this._detectGraphQLOperationType(parsed.query), operationName: parsed.operationName, variables: parsed.variables, extensions: parsed.extensions }; } } return void 0; } catch (error) { return void 0; } } /** * GraphQL ์˜คํผ๋ ˆ์ด์…˜ ํƒ€์ž… ๊ฐ์ง€ */ _detectGraphQLOperationType(query) { if (!query) return "query"; const trimmed = query.trim().toLowerCase(); if (trimmed.startsWith("mutation")) return "mutation"; if (trimmed.startsWith("subscription")) return "subscription"; return "query"; } /** * GraphQL ์˜คํผ๋ ˆ์ด์…˜ ์ด๋ฆ„ ์ถ”์ถœ */ _extractGraphQLOperationName(query) { if (!query) return void 0; const match = query.match(/(?:query|mutation|subscription)\s+(\w+)/); return match ? match[1] : void 0; } /** * ์š”์ฒญ ์šฐ์„ ์ˆœ์œ„ ๊ณ„์‚ฐ */ _calculateRequestPriority(url, method, graphql) { if (graphql?.operationType === "mutation") { return "high"; } if (["POST", "PUT", "DELETE"].includes(method.toUpperCase())) { return "high"; } if (url.includes("/api/") || url.includes("/graphql")) { return "medium"; } if (/\.(js|css|png|jpg|jpeg|gif|svg|woff|woff2|ttf)(\?|$)/.test(url)) { return "low"; } return "medium"; } /** * Fetch ์‘๋‹ต ์ฒ˜๋ฆฌ */ async _handleFetchResponse(response, request) { try { let responseBody; let responseBodySize = 0; if (this._config.captureResponseBody) { const responseClone = response.clone(); const bodyText = await responseClone.text(); responseBodySize = new TextEncoder().encode(bodyText).length; responseBody = bodyText.length > this._config.maxBodySize ? bodyText.substring(0, this._config.maxBodySize) + "...[truncated]" : bodyText; } const responseHeaders = this._extractResponseHeaders(response.headers); const graphqlResponse = request.graphql ? this._parseGraphQLResponse(responseBody) : void 0; const networkResponse = { id: generateUniqueId(), requestId: request.id, status: response.status, statusText: response.statusText, headers: responseHeaders, body: responseBody, timestamp: Date.now(), timing: request.timing, size: { responseHeaders: this._calculateHeadersSize(responseHeaders), responseBody: responseBodySize, total: this._calculateHeadersSize(responseHeaders) + responseBodySize }, graphql: graphqlResponse }; this._responseHistory.push(networkResponse); if (this._responseHistory.length > this._config.maxHistorySize) { this._responseHistory = this._responseHistory.slice(-this._config.maxHistorySize); } if (this._config.onResponse) { try { this._config.onResponse(networkResponse); } catch (error) { console.warn("[NetworkInterceptor] Error in onResponse callback:", error); } } } catch (error) { console.warn("[NetworkInterceptor] Error handling fetch response:", error); } } /** * Fetch ์—๋Ÿฌ ์ฒ˜๋ฆฌ */ async _handleFetchError(error, request) { const networkResponse = { id: generateUniqueId(), requestId: request.id, status: 0, statusText: "Network Error", headers: {}, body: error.message, timestamp: Date.now(), timing: request.timing, size: { responseHeaders: 0, responseBody: error.message.length, total: error.message.length } }; this._responseHistory.push(networkResponse); if (this._config.onError) { try { this._config.onError(error, request); } catch (callbackError) { console.warn("[NetworkInterceptor] Error in onError callback:", callbackError); } } } /** * XHR ์‘๋‹ต ์ฒ˜๋ฆฌ */ _handleXHRResponse(xhr, request) { try { let responseBody; let responseBodySize = 0; if (this._config.captureResponseBody) { responseBodySize = new TextEncoder().encode(xhr.responseText).length; responseBody = xhr.responseText.length > this._config.maxBodySize ? xhr.responseText.substring(0, this._config.maxBodySize) + "...[truncated]" : xhr.responseText; } const responseHeaders = {}; const headerString = xhr.getAllResponseHeaders(); if (headerString) { headerString.split("\r\n").forEach((line) => { const parts = line.split(": "); if (parts.length === 2) { responseHeaders[parts[0]] = parts[1]; } }); } const graphqlResponse = request.graphql ? this._parseGraphQLResponse(responseBody) : void 0; const networkResponse = { id: generateUniqueId(), requestId: request.id, status: xhr.status, statusText: xhr.statusText, headers: responseHeaders, body: responseBody, timestamp: Date.now(), timing: request.timing, size: { responseHeaders: this._calculateHeadersSize(responseHeaders), responseBody: responseBodySize, total: this._calculateHeadersSize(responseHeaders) + responseBodySize }, graphql: graphqlResponse }; this._responseHistory.push(networkResponse); if (this._config.onResponse) { try { this._config.onResponse(networkResponse); } catch (error) { console.warn("[NetworkInterceptor] Error in onResponse callback:", error); } } } catch (error) { console.warn("[NetworkInterceptor] Error handling XHR response:", error); } } /** * GraphQL ์‘๋‹ต ํŒŒ์‹ฑ */ _parseGraphQLResponse(body) { if (!body) return void 0; try { const parsed = JSON.parse(body); if (parsed.data !== void 0 || parsed.errors !== void 0) { return { errors: parsed.errors, data: parsed.data, extensions: parsed.extensions }; } } catch (error) { } return void 0; } /** * ํ—ค๋” ํฌ๊ธฐ ๊ณ„์‚ฐ */ _calculateHeadersSize(headers) { return Object.entries(headers).reduce((size, [key, value]) => { return size + key.length + value.length + 4; }, 0); } /** * ์š”์ฒญ ํ—ค๋” ์ถ”์ถœ */ _extractHeaders(headers) { const result = {}; if (!headers) { return result; } if (headers instanceof Headers) { headers.forEach((value, key) => { result[key] = value; }); } else if (Array.isArray(headers)) { headers.forEach(([key, value]) => { result[key] = value; }); } else { Object.entries(headers).forEach(([key, value]) => { result[key] = value; }); } return result; } /** * ์‘๋‹ต ํ—ค๋” ์ถ”์ถœ */ _extractResponseHeaders(headers) { const result = {}; headers.forEach((value, key) => { result[key] = value; }); return result; } /** * ํ˜„์žฌ ์ƒํƒœ ๊ฐ€์ ธ์˜ค๊ธฐ */ getState() { return { isIntercepting: this._isIntercepting, requestCount: this._requestHistory.length, responseCount: this._responseHistory.length, config: { ...this._config }, metrics: this.getNetworkMetrics() }; } /** * ์ •๋ฆฌ */ destroy() { this.stopIntercepting(); this.clearHistory(); } }; // src/network-interceptor/AxiosInterceptor.ts var AxiosInterceptor = class { constructor(callbacks) { this._isIntercepting = false; this._requestInterceptorId = null; this._responseInterceptorId = null; this._onRequest = callbacks?.onRequest; this._onResponse = callbacks?.onResponse; this._onError = callbacks?.onError; } /** * Axios ์ธํ„ฐ์…‰์…˜ ์‹œ์ž‘ */ startIntercepting() { if (this._isIntercepting) { console.warn("[AxiosInterceptor] Already intercepting"); return; } const axios = this._getAxiosInstance(); if (!axios) { console.warn("[AxiosInterceptor] Axios not found in global scope"); return; } console.log("[AxiosInterceptor] Starting axios interception..."); try { this._requestInterceptorId = axios.interceptors.request.use( (config) => this._handleAxiosRequest(config), (error) => this._handleAxiosRequestError(error) ); this._responseInterceptorId = axios.interceptors.response.use( (response) => this._handleAxiosResponse(response), (error) => this._handleAxiosResponseError(error) ); this._isIntercepting = true; console.log("[AxiosInterceptor] Axios interception started"); } catch (error) { console.error("[AxiosInterceptor] Failed to start interception:", error); } } /** * Axios ์ธํ„ฐ์…‰์…˜ ์ค‘์ง€ */ stopIntercepting() { if (!this._isIntercepting) { return; } const axios = this._getAxiosInstance(); if (!axios) { return; } console.log("[AxiosInterceptor] Stopping axios interception..."); try { if (this._requestInterceptorId !== null) { axios.interceptors.request.eject(this._requestInterceptorId); this._requestInterceptorId = null; } if (this._responseInterceptorId !== null) { axios.interceptors.response.eject(this._responseInterceptorId); this._responseInterceptorId = null; } this._isIntercepting = false; console.log("[AxiosInterceptor] Axios interception stopped"); } catch (error) { console.error("[AxiosInterceptor] Failed to stop interception:", error); } } /** * ์ „์—ญ axios ์ธ์Šคํ„ด์Šค ํƒ์ง€ */ _getAxiosInstance() { if (typeof window !== "undefined" && window.axios) { return window.axios; } if (typeof global !== "undefined" && global.axios) { return global.axios; } try { const moduleRequire = typeof require !== "undefined" ? require : null; if (moduleRequire) { return moduleRequire("axios"); } } catch (error) { } if (typeof window !== "undefined" && window.__AXIOS_INSTANCE__) { return window.__AXIOS_INSTANCE__; } return null; } /** * Axios ์š”์ฒญ ์ธํ„ฐ์…‰ํ„ฐ */ _handleAxiosRequest(config) { try { const requestId = generateUniqueId(); const startTime = performance.now(); const url = this._buildFullUrl(config); const method = (config.method || "GET").toUpperCase(); const headers = config.headers || {}; let requestBody; let requestBodySize = 0; if (config.data) { if (typeof config.data === "string") { requestBody = config.data; requestBodySize = new TextEncoder().encode(config.data).length; } else if (config.data instanceof FormData) { requestBody = "[FormData]"; requestBodySize = 0; } else { try { requestBody = JSON.stringify(config.data); requestBodySize = new TextEncoder().encode(requestBody).length; } catch (error) { requestBody = "[Unserializable Data]"; requestBodySize = 0; } } } const timing = { startTime, requestStart: startTime, responseStart: 0, responseEnd: 0, duration: 0 }; const enhancedRequest = { id: requestId, url, method, headers: this._normalizeHeaders(headers), body: requestBody, timestamp: Date.now(), timing, size: { requestHeaders: this._calculateHeadersSize(headers), requestBody: requestBodySize, total: this._calculateHeadersSize(headers) + requestBodySize }, graphql: this._detectGraphQL(headers, requestBody), priority: this._calculatePriority(url, method) }; config._debugTimeMachine = { requestId, startTime, enhancedRequest }; if (this._onRequest) { try { this._onRequest(enhancedRequest); } catch (error) { console.warn("[AxiosInterceptor] Error in onRequest callback:", error); } } return config; } catch (error) { console.error("[AxiosInterceptor] Error in request interceptor:", error); return config; } } /** * Axios ์š”์ฒญ ์—๋Ÿฌ ์ธํ„ฐ์…‰ํ„ฐ */ _handleAxiosRequestError(error) { console.error("[AxiosInterceptor] Request error:", error); return Promise.reject(error); } /** * Axios ์‘๋‹ต ์ธํ„ฐ์…‰ํ„ฐ */ _handleAxiosResponse(response) { try { const config = response.config; const metadata = config._debugTimeMachine; if (!metadata) { return response; } const responseEndTime = performance.now(); metadata.enhancedRequest.timing.responseStart = responseEndTime - 10; metadata.enhancedRequest.timing.responseEnd = responseEndTime; metadata.enhancedRequest.timing.duration = responseEndTime - metadata.startTime; let responseBody; let responseBodySize = 0; if (response.data) { if (typeof response.data === "string") { responseBody = response.data; responseBodySize = new TextEncoder().encode(response.data).length; } else { try { responseBody = JSON.stringify(response.data); responseBodySize = new TextEncoder().encode(responseBody).length; } catch (error) { responseBody = "[Unserializable Data]"; responseBodySize = 0; } } } const responseHeaders = this._normalizeHeaders(response.headers || {}); const enhancedResponse = { id: generateUniqueId(), requestId: metadata.requestId, status: response.status, statusText: response.statusText || "", headers: responseHeaders, body: responseBody, timestamp: Date.now(), timing: metadata.enhancedRequest.timing, size: { responseHeaders: this._calculateHeadersSize(responseHeaders), responseBody: responseBodySize, total: this._calculateHeadersSize(responseHeaders) + responseBodySize }, graphql: metadata.enhancedRequest.graphql ? this._parseGraphQLResponse(responseBody) : void 0 }; if (this._onResponse) { try { this._onResponse(enhancedResponse); } catch (error) { console.warn("[AxiosInterceptor] Error in onResponse callback:", error); } } return response; } catch (error) { console.error("[AxiosInterceptor] Error in response interceptor:", error); return response; } } /** * Axios ์‘๋‹ต ์—๋Ÿฌ ์ธํ„ฐ์…‰ํ„ฐ */ _handleAxiosResponseError(error) { try { const config = error.config; const metadata = config?._debugTimeMachine; if (metadata) { const responseEndTime = performance.now(); metadata.enhancedRequest.timing.responseEnd = responseEndTime; metadata.enhancedRequest.timing.duration = responseEndTime - metadata.startTime; const enhancedResponse = { id: generateUniqueId(), requestId: metadata.requestId, status: error.response?.status || 0, statusText: error.response?.statusText || error.message, headers: this._normalizeHeaders(error.response?.headers || {}), body: error.message, timestamp: Date.now(), timing: metadata.enhancedRequest.timing, size: { responseHeaders: this._calculateHeadersSize(error.response?.headers || {}), responseBody: error.message.length, total: this._calculateHeadersSize(error.response?.headers || {}) + error.message.length } }; if (this._onError) { try { this._onError(error, metadata.enhancedRequest); } catch (callbackError) { console.warn("[AxiosInterceptor] Error in onError callback:", callbackError); } } if (this._onResponse) { try { this._onResponse(enhancedResponse); } catch (callbackError) { console.warn("[AxiosInterceptor] Error in onResponse callback:", callbackError); } } } } catch (interceptorError) { console.error("[AxiosInterceptor] Error in response error interceptor:", interceptorError); } return Promise.reject(error); } /** * ์ „์ฒด URL ๊ตฌ์„ฑ */ _buildFullUrl(config) { const baseURL = config.baseURL || ""; const url = config.url || ""; if (url.startsWith("http://") || url.startsWith("https://")) { return url; } return baseURL + url; } /** * ํ—ค๋” ์ •๊ทœํ™” */ _normalizeHeaders(headers) { const normalized = {}; if (!headers) { return normalized; } Object.entries(headers).forEach(([key, value]) => { if (typeof value === "string") { normalized[key] = value; } else if (value != null) { normalized[key] = String(value); } }); return normalized; } /** * ํ—ค๋” ํฌ๊ธฐ ๊ณ„์‚ฐ */ _calculateHeadersSize(headers) { const normalized = this._normalizeHeaders(headers); return Object.entries(normalized).reduce((size, [key, value]) => { return size + key.length + value.length + 4; }, 0); } /** * GraphQL ๊ฐ์ง€ */ _detectGraphQL(headers, body) { const normalized = this._normalizeHeaders(headers); const contentType = normalized["content-type"] || normalized["Content-Type"] || ""; if (contentType.includes("application/graphql")) { return { operationType: this._detectGraphQLOperationType(body || ""), operationName: this._extractGraphQLOperationName(body || "") }; } if (contentType.includes("application/json") && body) { try { const parsed = JSON.parse(body); if (parsed.query || parsed.operationName) { return { operationType: this._detectGraphQLOperationType(parsed.query), operationName: parsed.operationName, variables: parsed.variables, extensions: parsed.extensions }; } } catch (error) { } } return void 0; } /** * GraphQL ์˜คํผ๋ ˆ์ด์…˜ ํƒ€์ž… ๊ฐ์ง€ */ _detectGraphQLOperationType(query) { if (!query) return "query"; const trimmed = query.trim().toLowerCase(); if (trimmed.startsWith("mutation")) return "mutation"; if (trimmed.startsWith("subscription")) return "subscription"; return "query"; } /** * GraphQL ์˜คํผ๋ ˆ์ด์…˜ ์ด๋ฆ„ ์ถ”์ถœ */ _extractGraphQLOperationName(query) { if (!query) return void 0; const match = query.match(/(?:query|mutation|subscription)\s+(\w+)/); return match ? match[1] : void 0; } /** * GraphQL ์‘๋‹ต ํŒŒ์‹ฑ */ _parseGraphQLResponse(body) { if (!body) return void 0; try { const parsed = JSON.parse(body); if (parsed.data !== void 0 || parsed.errors !== void 0) { return { errors: parsed.errors, data: parsed.data, extensions: parsed.extensions }; } } catch (error) { } return void 0; } /** * ์š”์ฒญ ์šฐ์„ ์ˆœ์œ„ ๊ณ„์‚ฐ */ _calculatePriority(url, method) { if (["POST", "PUT", "DELETE"].includes(method.toUpperCase())) { return "high"; } if (url.includes("/api/") || url.includes("/graphql")) { return "medium"; } return "low"; } /** * ํ˜„์žฌ ์ƒํƒœ ํ™•์ธ */ get isIntercepting() { return this._isIntercepting; } /** * ์ƒํƒœ ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ */ getState() { return { isIntercepting: this._isIntercepting, hasRequestInterceptor: this._requestInterceptorId !== null, hasResponseInterceptor: this._responseInterceptorId !== null, axiosAvailable: this._getAxiosInstance() !== null }; } /** * ์ •๋ฆฌ */ destroy() { this.stopIntercepting(); } }; // src/network-interceptor/WebSocketConnector.ts var WebSocketConnector = class { constructor(config) { this._ws = null; this._isConnected = false; this._reconnectAttempts = 0; this._heartbeatTimer = null; this._reconnectTimer = null; this._messageQueue = []; this._config = { reconnectInterval: 3e3, maxReconnectAttempts: 10, enableHeartbeat: true, heartbeatInterval: 3e4, sessionId: this._generateSessionId(), ...config }; this._sessionId = this._config.sessionId; console.log(`[WebSocketConnector] Initialized with session: ${this._sessionId}`); } /** * WebSocket ์—ฐ๊ฒฐ ์‹œ์ž‘ */ async connect() { if (this._isConnected && this._ws) { console.log("[WebSocketConnector] Already connected"); return; } console.log(`[WebSocketConnector] Connecting to ${this._config.url}...`); try { this._ws = new WebSocket(this._config.url); this._ws.onopen = this._onOpen.bind(this); this._ws.onclose = this._onClose.bind(this); this._ws.onerror = this._onError.bind(this); this._ws.onmessage = this._onMessage.bind(this); return new Promise((resolve, reject) => { const timeout = setTimeout(() => { reject(new Error("WebSocket connection timeout")); }, 1e4); this._ws.addEventListener("open", () => { clearTimeout(timeout); resolve(); }); this._ws.addEventListener("error", (error) => { clearTimeout(timeout); reject(error); }); }); } catch (error) { console.error("[WebSocketConnector] Connection failed:", error); throw error; } } /** * WebSocket ์—ฐ๊ฒฐ ํ•ด์ œ */ disconnect() { console.log("[WebSocketConnector] Disconnecting..."); this._clearTimers(); if (this._ws) { this._ws.onopen = null; this._ws.onclose = null; this._ws.onerror = null; this._ws.onmessage = null; if (this._ws.readyState === WebSocket.OPEN) { this._ws.close(1e3, "Manual disconnect"); } this._ws = null; } this._isConnected = false; this._reconnectAttempts = 0; console.log("[WebSocketConnector] Disconnected"); } /** * ๋„คํŠธ์›Œํฌ ์š”์ฒญ ์ „์†ก */ sendNetworkRequest(request) { const message = { type: "network-request", data: request, timestamp: Date.now(), sessionId: this._sessionId }; this._sendMessage(message); } /** * ๋„คํŠธ์›Œํฌ ์‘๋‹ต ์ „์†ก */ sendNetworkResponse(response) { const message = { type: "network-response", data: response, timestamp: Date.now(), sessionId: this._sessionId }; this._sendMessage(message); } /** * ์—๋Ÿฌ ์ „์†ก */ sendError(error, request) { const message = { type: "error", data: { message: error.message, stack: error.stack, request }, timestamp: Date.now(), sessionId: this._sessionId }; this._sendMessage(message); } /** * ์—ฐ๊ฒฐ ์ƒํƒœ ํ™•์ธ */ get isConnected() { return this._isConnected && this._ws?.readyState === WebSocket.OPEN; } /** * ์„ธ์…˜ ID ๊ฐ€์ ธ์˜ค๊ธฐ */ get sessionId() { return this._sessionId; } /** * ๋Œ€๊ธฐ ์ค‘์ธ ๋ฉ”์‹œ์ง€ ์ˆ˜ */ get queueSize() { return this._messageQueue.length; } /** * WebSocket ์—ด๋ฆผ ์ด๋ฒคํŠธ */ _onOpen(event) { console.log("[WebSocketConnector] Connected successfully"); this._isConnected = true; this._reconnectAttempts = 0; this._flushMessageQueue(); if (this._config.enableHeartbeat) { this._startHeartbeat(); } } /** * WebSocket ๋‹ซํž˜ ์ด๋ฒคํŠธ */ _onClose(event) { console.log(`[WebSocketConnector] Connection closed: ${event.code} ${event.reason}`); this._isConnected = false; this._clearTimers(); if (event.code !== 1e3 && this._reconnectAttempts < this._config.maxReconnectAttempts) { this._scheduleReconnect(); } } /** * WebSocket ์—๋Ÿฌ ์ด๋ฒคํŠธ */ _onError(event) { console.error("[WebSocketConnector] WebSocket error:", event); if (!this._isConnected && this._reconnectAttempts < this._config.maxReconnectAttempts) { this._scheduleReconnect(); } } /** * WebSocket ๋ฉ”์‹œ์ง€ ์ˆ˜์‹  */ _onMessage(event) { try { const message = JSON.parse(event.data); if (message.type === "ping") { this._sendMessage({ type: "pong", data: message.data, timestamp: Date.now(), sessionId: this._sessionId }); } } catch (error) { console.warn("[WebSocketConnector] Failed to parse message:", error); } } /** * ๋ฉ”์‹œ์ง€ ์ „์†ก */ _sendMessage(message) { if (!this.isConnected) { this._messageQueue.push(message); if (this._messageQueue.length > 1e3) { this._messageQueue = this._messageQueue.slice(-1e3); } if (!this._isConnected && this._reconnectAttempts === 0) { this._scheduleReconnect(); } return; } try { const serialized = JSON.stringify(message); this._ws.send(serialized); } catch (error) { console.error("[WebSocketConnector] Failed to send message:", error); this._messageQueue.unshift(message); } } /** * ๋Œ€๊ธฐ ์ค‘์ธ ๋ฉ”์‹œ์ง€๋“ค ์ „์†ก */ _flushMessageQueue() { console.log(`[WebSocketConnector] Flushing ${this._messageQueue.length} queued messages`); const messages = [...this._messageQueue]; this._messageQueue = []; messages.forEach((message) => { this._sendMessage(message); }); } /** * ์žฌ์—ฐ๊ฒฐ ์Šค์ผ€์ค„๋ง */ _scheduleReconnect() { if (this._reconnectTimer) { return; } this._reconnectAttempts++; const delay = this._config.reconnectInterval * Math.pow(1.5, this._reconnectAttempts - 1); console.log(`[WebSocketConnector] Scheduling reconnect attempt ${this._reconnectAttempts}/${this._config.maxReconnectAttempts} in ${delay}ms`); this._reconnectTimer = setTimeout(async () => { this._reconnectTimer = null; try { await this.connect(); } catch (error) { console.error("[WebSocketConnector] Reconnect failed:", error); if (this._reconnectAttempts < this._config.maxReconnectAttempts) { this._scheduleReconnect(); } else { console.error("[WebSocketConnector] Max reconnect attempts reached"); } } }, delay); } /** * ํ•˜ํŠธ๋น„ํŠธ ์‹œ์ž‘ */ _startHeartbeat() { this._heartbeatTimer = setInterval(() => { if (this.isConnected) { this._sendMessage({ type: "ping", data: { sessionId: this._sessionId }, timestamp: Date.now(), sessionId: this._sessionId }); } }, this._config.heartbeatInterval); } /** * ํƒ€์ด๋จธ๋“ค ์ •๋ฆฌ */ _clearTimers() { if (this._heartbeatTimer) { clearInterval(this._heartbeatTimer); this._heartbeatTimer = null; } if (this._reconnectTimer) { clearTimeout(this._reconnectTimer); this._reconnectTimer = null; } } /** * ์„ธ์…˜ ID ์ƒ์„ฑ */ _generateSessionId() { return `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } /** * ์ƒํƒœ ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ */ getState() { return { isConnected: this._isConnected, sessionId: this._sessionId, queueSize: this._messageQueue.length, reconnectAttempts: this._reconnectAttempts, websocketState: this._ws?.readyState }; } /** * ์ •๋ฆฌ */ destroy() { this.disconnect(); this._messageQueue = []; } }; // src/network-interceptor/RealTimeAPIInterceptor.ts var RealTimeAPIInterceptor = class { constructor(config = {}) { this._axiosInterceptor = null; this._webSocketConnector = null; this._isActive = false; this._stats = { totalRequests: 0, totalResponses: 0, totalErrors: 0, startTime: 0 }; this._config = { // NetworkInterceptor ๊ธฐ๋ณธ ์„ค์ • maxHistorySize: 1e3, captureRequestBody: true, captureResponseBody: true, enableTiming: true, enableGraphQLParsing: true, enableAxiosInterception: true, maxBodySize: 1024 * 1024, // 1MB excludePatterns: [ /chrome-extension:/, /moz-extension:/, /webkit-masked-url:/, /^data:/, /\.woff2?(\?|$)/, /\.ttf(\?|$)/, /favicon\.ico/, /sockjs/, /websocket/ ], includePatterns: [], onRequest: void 0, onResponse: void 0, onError: void 0, // RealTimeAPIInterceptor ์„ค์ • websocket: { url: "ws://localhost:8080/ws", reconnectInterval: 3e3, maxReconnectAttempts: 10, enableHeartbeat: true, heartbeatInterval: 3e4, ...config.websocket }, autoConnect: true, enableLogging: true, ...config }; this._networkInterceptor = new NetworkInterceptor({ ...this._config, onRequest: this._handleNetworkRequest.bind(this), onResponse: this._handleNetworkResponse.bind(this), onError: this._handleNetworkError.bind(this) }); if (this._config.enableAxiosInterception) { this._axiosInterceptor = new AxiosInterceptor({ onRequest: this._handleNetworkRequest.bind(this), onResponse: this._handleNetworkResponse.bind(this), onError: this._handleNetworkError.bind(this) }); } if (this._config.websocket.url) { this._webSocketConnector = new WebSocketConnector(this._config.websocket); } this._log("RealTimeAPIInterceptor initialized"); } /** * ์‹ค์‹œ๊ฐ„ API ์ธํ„ฐ์…‰์…˜ ์‹œ์ž‘ */ async start() { if (this._isActive) { this._log("Already active"); return; } this._log("Starting real-time API interception..."); this._stats.startTime = Date.now(); try { if (this._webSocketConnector && this._config.autoConnect) { try { await this._webSocketConnector.connect(); this._log("WebSocket connected successfully"); } catch (error) { this._log("WebSocket connection failed, continuing without real-time sync:", error); } } this._networkInterceptor.startIntercepting(); this._log("Network interception started"); if (this._axiosInterceptor) { this._axiosInterceptor.startIntercepting(); this._log("Axios interception started"); } this._isActive = true; this._log("Real-time API interception active"); this._sendStartEvent(); } catch (error) { this._log("Failed to start real-time API interception:", error); throw error; } } /** * ์‹ค์‹œ๊ฐ„ API ์ธํ„ฐ์…‰์…˜ ์ค‘์ง€ */ async stop() { if (!this._isActive) { return; } this._log("Stopping real-time API interception..."); try { this._networkInterceptor.stopIntercepting(); this._log("Network interception stopped"); if (this._axiosInterceptor) { this._axiosInterceptor.stopIntercepting(); this._log("Axios interception stopped"); } this._sendStopEvent(); if (this._webSocketConnector) { this._webSocketConnector.disconnect(); this._log("WebSocket disconnected"); } this._isActive = false; this._log("Real-time API interception stopped"); } catch (error) { this._log("Error while stopping real-time API interception:", error); throw error; } } /** * WebSocket ์ˆ˜๋™ ์—ฐ๊ฒฐ */ async connectWebSocket() { if (!this._webSocketConnector) { throw new Error("WebSocket connector not configured"); } await this._webSocketConnector.connect(); this._log("WebSocket manually connected"); } /** * WebSocket ์—ฐ๊ฒฐ ํ•ด์ œ */ disconnectWebSocket() { if (this._webSocketConnector) { this._webSocketConnector.disconnect(); this._log("WebSocket manually disconnected"); } } /** * ์š”์ฒญ ํžˆ์Šคํ† ๋ฆฌ ๊ฐ€์ ธ์˜ค๊ธฐ */ getRequestHistory() { return this._networkInterceptor.getRequestHistory(); } /** * ์‘๋‹ต ํžˆ์Šคํ† ๋ฆฌ ๊ฐ€์ ธ์˜ค๊ธฐ */ getResponseHistory() { return this._networkInterceptor.getResponseHistory(); } /** * ํžˆ์Šคํ† ๋ฆฌ ์ดˆ๊ธฐํ™” */ clearHistory() { this._networkInterceptor.clearHistory(); this._stats = { totalRequests: 0, totalResponses: 0, totalErrors: 0, startTime: Date.now() }; this._log("History cleared"); } /** * ํ†ต๊ณ„ ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ */ getStats() { const networkMetrics = this._networkInterceptor.getNetworkMetrics(); const uptime = Date.now() - this._stats.startTime; return { ...this._stats, uptime, networkMetrics, isActive: this._isActive, webSocketConnected: this._webSocketConnector?.isConnected || false, webSocketQueueSize: this._webSocketConnector?.queueSize || 0, axiosIntercepting: this._axiosInterceptor?.isIntercepting || false }; } /** * ํ˜„์žฌ ์ƒํƒœ ์ •๋ณด */ getState() { return { isActive: this._isActive, config: { ...this._config }, stats: this.getStats(), networkInterceptor: this._networkInterceptor.getState(), axiosInterceptor: this._axiosInterceptor?.getState(), webSocketConnector: this._webSocketConnector?.getState() }; } /** * ๋„คํŠธ์›Œํฌ ์š”์ฒญ ์ฒ˜๋ฆฌ */ _handleNetworkRequest(request) { this._stats.totalRequests++; this._log(`\u{1F4E4} Request: ${request.method} ${request.url}`, { id: request.id, timing: request.timing, size: request.size, graphql: request.graphql }); if (this._webSocketConnector?.isConnected) { this._webSocketConnector.sendNetworkRequest(request); } } /** * ๋„คํŠธ์›Œํฌ ์‘๋‹ต ์ฒ˜๋ฆฌ */ _handleNetworkResponse(response) { this._stats.totalResponses++; this._log(`\u{1F4E5} Response: ${response.status} ${response.statusText}`, { id: response.id, requestId: response.requestId, timing: response.timing, size: response.size }); if (this._webSocketConnector?.isConnected) { this._webSocketConnector.sendNetworkResponse(response); } } /** * ๋„คํŠธ์›Œํฌ ์—๋Ÿฌ ์ฒ˜๋ฆฌ */ _handleNetworkError(error, request) { this._stats.totalErrors++; this._log(`\u274C Error: ${error.message}`, { requestId: request.id, url: request.url, method: request.method, error: error.message }); if (this._webSocketConnector?.isConnected) { this._webSocketConnector.sendError(error, request); } } /** * ์‹œ์ž‘ ์ด๋ฒคํŠธ ์ „์†ก */ _sendStartEvent() { if (this._webSocketConnector?.isConnected) { this._webSocketConnector.sendNetworkRequest({ id: `start_${Date.now()}`, url: "debug-time-machine://interceptor/start", method: "EVENT", headers: {}, timestamp: Date.now(), timing: { startTime: performance.now(), requestStart: performance.now(), responseStart: 0, responseEnd: 0, duration: 0 }, size: { requestHeaders: 0, requestBody: 0, total: 0 }, priority: "high" }); } } /** * ์ข…๋ฃŒ ์ด๋ฒคํŠธ ์ „์†ก */ _sendStopEvent() { if (this._webSocketConnector?.isConnected) { this._webSocketConnector.sendNetworkRequest({ id: `stop_${Date.now()}`, url: "debug-time-machine://interceptor/stop", method: "EVENT", headers: {}, timestamp: Date.now(), timing: { startTime: performance.now(), requestStart: performance.now(), responseStart: 0, responseEnd: 0, duration: 0 }, size: { requestHeaders: