UNPKG

react-native-debug-toolkit

Version:

A simple yet powerful debugging toolkit for React Native with a convenient floating UI for development

390 lines (322 loc) 9.95 kB
import React from 'react' class NetworkFeature { static instance = null static MAX_LOGS = 200 constructor() { if (NetworkFeature.instance) { return NetworkFeature.instance } this.logs = [] this.pendingAxiosRequests = new Map() this.originalFetch = null this.blacklist = [] // URL patterns to exclude from logging NetworkFeature.instance = this } setup() { // this._interceptFetch() return this } getData() { return this.logs } cleanup() { if (this.originalFetch) { global.fetch = this.originalFetch this.originalFetch = null } this.logs = [] this.pendingAxiosRequests.clear() } // Check if a URL matches any pattern in the blacklist isUrlBlacklisted(url) { if (!url) return false return this.blacklist.some(pattern => { if (pattern instanceof RegExp) { return pattern.test(url) } return url.includes(pattern) }) } // Add a URL pattern to blacklist addUrlToBlacklist(pattern) { if (!this.blacklist.some(p => (p instanceof RegExp && pattern instanceof RegExp) ? p.toString() === pattern.toString() : p === pattern)) { this.blacklist.push(pattern) } } // Remove a URL pattern from blacklist removeUrlFromBlacklist(pattern) { this.blacklist = this.blacklist.filter(p => (p instanceof RegExp && pattern instanceof RegExp) ? p.toString() !== pattern.toString() : p !== pattern) } // Clear all patterns from blacklist clearBlacklist() { this.blacklist = [] } setupAxiosInterceptors(axiosInstance) { axiosInstance.interceptors.request.use( (config) => { // Generate a unique ID if one doesn't exist if (!config.headers) { config.headers = {} } if (!config.headers['X-Request-Id']) { config.headers['X-Request-Id'] = Date.now().toString() + Math.random().toString(36).substring(2, 10) } const trackId = config.headers['X-Request-Id'] this.pendingAxiosRequests.set(trackId, { timestamp: new Date(), startTime: Date.now(), }) return config }, (error) => { this.logAxiosError(error) return Promise.reject(error) }, ) axiosInstance.interceptors.response.use( (response) => { this.logAxiosResponse(response) return response }, (error) => { this.logAxiosError(error) return Promise.reject(error) }, ) } logAxiosResponse(response) { if (!response || !response.config || !response.config.headers) { return } const trackId = response.config.headers['X-Request-Id'] const pendingRequest = this.pendingAxiosRequests.get(trackId) if (!pendingRequest) { return } const url = `${response.config.baseURL || ''}${response.config.url}` // Skip logging if URL is blacklisted if (this.isUrlBlacklisted(url)) { this.pendingAxiosRequests.delete(trackId) return } if (this.logs.length >= NetworkFeature.MAX_LOGS) { this.logs.shift() } // Calculate duration const duration = Date.now() - pendingRequest.startTime const logEntry = { timestamp: pendingRequest.timestamp, duration: Math.round(duration), request: { url: url, method: response.config.method?.toUpperCase() || 'GET', headers: response.config.headers, body: response.config.data || response.config.params, }, response: { status: response.status, statusText: response.statusText, headers: response.headers, }, } if (response.data && typeof response.data === 'object') { logEntry.response.success = response.data.success !== false logEntry.response.data = response.data } else { logEntry.response.success = response.status >= 200 && response.status < 300 if (response.data) { logEntry.response.data = response.data } } this.logs.push(logEntry) this.pendingAxiosRequests.delete(trackId) } logAxiosError(error) { if (!error.config) { return } if (!error.config.headers) { error.config.headers = {} } const trackId = error.config.headers['X-Request-Id'] const pendingRequest = this.pendingAxiosRequests.get(trackId) const startTime = pendingRequest ? pendingRequest.startTime : Date.now() - 100 const url = `${error.config.baseURL || ''}${error.config.url}` // Skip logging if URL is blacklisted if (this.isUrlBlacklisted(url)) { this.pendingAxiosRequests.delete(trackId) return } if (this.logs.length >= NetworkFeature.MAX_LOGS) { this.logs.shift() } const duration = Date.now() - startTime const logEntry = { timestamp: pendingRequest ? pendingRequest.timestamp : new Date(), duration: Math.round(duration), request: { url: url, method: error.config.method?.toUpperCase() || 'GET', headers: error.config.headers, body: error.config.data || error.config.params, }, error: error.message, success: false, } if (error.response && error.response.data) { logEntry.response = { status: error.response.status, statusText: error.response.statusText, headers: error.response.headers, data: error.response.data, success: false, } } this.logs.push(logEntry) this.pendingAxiosRequests.delete(trackId) } _interceptFetch() { this.originalFetch = global.fetch global.fetch = async (...args) => { const request = args[0] const options = args[1] || {} const startTime = Date.now() try { const response = await this.originalFetch(...args) this._logFetchResponse(request, options, response.clone(), startTime) return response } catch (error) { this._logFetchError(request, options, error, startTime) throw error } } } _logFetchResponse(request, options, response, startTime) { const requestUrl = typeof request === 'string' ? request : request.url // Skip logging if URL is blacklisted if (this.isUrlBlacklisted(requestUrl)) { return } if (this.logs.length >= NetworkFeature.MAX_LOGS) { this.logs.shift() } const duration = Date.now() - startTime const commonData = { timestamp: new Date(), duration: Math.round(duration), request: { url: requestUrl, method: options.method || 'GET', headers: options.headers || {}, body: options.body, }, } // Try to parse JSON response response .text() .then((text) => { let responseData let success = response.status >= 200 && response.status < 300 try { // Try to parse as JSON if (text) { responseData = JSON.parse(text) // Use API success flag if available if (responseData && typeof responseData.success !== 'undefined') { success = !!responseData.success } } } catch (e) { // Not JSON, use text response responseData = text } const logEntry = { ...commonData, response: { status: response.status, statusText: response.statusText, headers: this._headersToObject(response.headers), data: responseData, success: success, }, } this.logs.push(logEntry) }) .catch((err) => { // Fallback for when we can't read the response body const logEntry = { ...commonData, response: { status: response.status, statusText: response.statusText, headers: this._headersToObject(response.headers), success: response.status >= 200 && response.status < 300, }, } this.logs.push(logEntry) }) } _logFetchError(request, options, error, startTime) { const requestUrl = typeof request === 'string' ? request : request.url // Skip logging if URL is blacklisted if (this.isUrlBlacklisted(requestUrl)) { return } if (this.logs.length >= NetworkFeature.MAX_LOGS) { this.logs.shift() } const duration = Date.now() - startTime this.logs.push({ timestamp: new Date(), duration: Math.round(duration), request: { url: requestUrl, method: options.method || 'GET', headers: options.headers || {}, body: options.body, }, error: error.message, success: false, }) } // Utility to convert Headers object to plain object _headersToObject(headers) { const result = {} if (headers && typeof headers.forEach === 'function') { headers.forEach((value, key) => { result[key] = value }) } else if (headers) { // Fallback for non-standard headers object Object.keys(headers).forEach((key) => { result[key] = headers[key] }) } return result } } export const createNetworkFeature = () => { const feature = new NetworkFeature() return { name: 'network', label: 'Network Logs', setup: () => feature.setup(), getData: () => feature.getData(), cleanup: () => feature.cleanup(), setupAxiosInterceptors: (axiosInstance) => feature.setupAxiosInterceptors(axiosInstance), // Expose blacklist management methods addUrlToBlacklist: (pattern) => feature.addUrlToBlacklist(pattern), removeUrlFromBlacklist: (pattern) => feature.removeUrlFromBlacklist(pattern), clearBlacklist: () => feature.clearBlacklist(), getBlacklist: () => feature.blacklist, } }