debug-time-machine-core
Version:
๐ฐ๏ธ Debug Time Machine Core - ์๊ฐ์ฌํ ์์ง๊ณผ ๋คํธ์ํฌ ์ธํฐ์ ํฐ
1,686 lines (1,680 loc) โข 157 kB
JavaScript
"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: