UNPKG

error-flux

Version:

Network request interceptor and logger for web applications

229 lines (210 loc) 7.27 kB
/** * ErrorFlux Network Interceptor (MonkeyPatch Approach) * Captures all XHR & Fetch calls, logging success & error cases. */ import { getNetworkLogs, saveNetworkLogs } from "../db/index"; import store from "../state/store"; import { NetWorkClient, NetworkLog, StorageType, StorageTypes } from "../types"; import genUUID from "../utils/gen-uuid"; import { runLowPriorityTask } from "../utils/low-priority"; function getAllResponseHeaders(xhr: XMLHttpRequest): Record<string, string> { const headers = {}; const headerPairs = xhr.getAllResponseHeaders().trim().split("\n"); for (let i = 0; i < headerPairs.length; i++) { const headerPair = headerPairs[i].trim().split(": "); const key = headerPair[0]; const value = headerPair[1]; (headers as Record<string, string>)[key] = value; } return headers as Record<string, string>; } // Utility to generate unique request IDs function generateRequestId(): string { return genUUID(); } const logs = { async push(log: NetworkLog) { runLowPriorityTask(async () => { const { storeName: _storeName, dbName, storageType } = store.getState(); const storeName = _storeName.networkLogs; switch (storageType) { case StorageTypes.LocalStorage: try { const dbData = JSON.parse(localStorage.getItem(dbName) || "{}"); const existingLogs = dbData[storeName] || []; existingLogs.push(log); dbData[storeName] = existingLogs; localStorage.setItem(dbName, JSON.stringify(dbData)); } catch (err) { console.warn("Failed to save to localStorage:", err); } break; case StorageTypes.SessionStorage: try { const dbData = JSON.parse(sessionStorage.getItem(dbName) || "{}"); const existingLogs = dbData[storeName] || []; existingLogs.push(log); dbData[storeName] = existingLogs; sessionStorage.setItem(dbName, JSON.stringify(dbData)); } catch (err) { console.warn("Failed to save to sessionStorage:", err); } break; case StorageTypes.IndexedDB: await saveNetworkLogs(log); break; } }); }, }; const errorFluxNetworkInterceptor = ({ pattern, onlyFailures = false, }: { pattern: string | RegExp; onlyFailures?: boolean; }) => { const originalFetch = window.fetch; const originalXHR = window.XMLHttpRequest; window.fetch = async function (input: RequestInfo | URL, init?: RequestInit) { const requestId = generateRequestId(); const startTime = performance.now(); return originalFetch(input, init) .then(async (response) => { const clonedResponse = response.clone(); const responseData = await clonedResponse.text(); if ( clonedResponse.url.match(pattern) && (!onlyFailures || (onlyFailures && !clonedResponse.ok)) ) { logs.push({ id: requestId, type: NetWorkClient.Fetch, url: typeof input === "string" ? input : input.toString(), method: init?.method || "GET", requestHeaders: init?.headers ? Object.fromEntries(new Headers(init.headers).entries()) : {}, requestBody: init?.body || null, responseHeaders: Object.fromEntries( clonedResponse.headers.entries() ), responseBody: responseData, status: clonedResponse.status, duration: performance.now() - startTime, success: clonedResponse.ok, cookies: document.cookie, }); } return response; }) .catch((error: Error) => { // For fetch API, we can determine network errors (status 0) vs HTTP errors (400-500) const status = error instanceof TypeError ? 0 : (error as any).status || ((error as any).response && (error as any).response.status) || 500; if ((error as any).response.url.match(pattern)) { logs.push({ id: requestId, type: NetWorkClient.Fetch, url: typeof input === "string" ? input : input.toString(), method: init?.method || "GET", error: error.message, duration: performance.now() - startTime, success: false, requestBody: null, status, // Actual HTTP error status or 500 as fallback cookies: document.cookie, }); } throw error; }); }; /** * MonkeyPatch XMLHttpRequest */ class InterceptedXHR extends originalXHR { private requestId: string; private startTime: number | null; private _url: string; private _method: string; private _error: string | null; constructor() { super(); this.requestId = generateRequestId(); this.startTime = null; this._url = ""; this._method = ""; this._error = null; } open(method: string, url: string, ...args: any[]) { this._method = method; this._url = url; // @ts-ignore super.open(method, url, ...(args as any)); } send(body: Document | XMLHttpRequestBodyInit | null) { this.startTime = performance.now(); this.addEventListener("loadend", () => { const isFailure = !(this.status >= 200 && this.status < 300); if (!this._url.match(pattern) || (onlyFailures && !isFailure)) { return; } logs.push({ id: this.requestId, type: NetWorkClient.XHR, url: this._url, method: this._method, requestBody: body || null, responseHeaders: getAllResponseHeaders(this), responseBody: this.responseText, status: this.status, duration: performance.now() - (this.startTime as number), success: !isFailure, cookies: document.cookie, }); }); this.addEventListener("error", () => { if (this._url.match(pattern)) { logs.push({ id: this.requestId, type: NetWorkClient.XHR, url: this._url, method: this._method, error: "Network error", duration: performance.now() - (this.startTime as number), success: false, requestBody: null, cookies: document.cookie, status: this.status, }); } }); super.send(body); } } window.XMLHttpRequest = InterceptedXHR; const getLogs = async () => { const { storeName: storeNameObj, dbName, storageType } = store.getState(); const storeName = storeNameObj.networkLogs; switch (storageType) { case StorageTypes.IndexedDB: return await getNetworkLogs(); case StorageTypes.LocalStorage: return ( JSON.parse(localStorage.getItem(dbName) || "{}")?.[storeName] || [] ); case StorageTypes.SessionStorage: return ( JSON.parse(sessionStorage.getItem(dbName) || "{}")?.[storeName] || [] ); } }; return { getLogs, }; }; export default errorFluxNetworkInterceptor;